Examining the difference in election outcome between FPTP and D’Hondt (proportional representation).
knitr::include_graphics("preview.jpg")

In this short article I am exploring what the Dutch political landscape would look like if it operated under a first-past-the-post (FPTP) system.
The election data for the Netherlands was downloaded from the Kiesraad. The shapefile was downloaded from ESRI NL.
These are the 2021 results summarised:
#sort in long format
df_results[is.na(df_results)] <- 0
df_pr <- df_results %>%
pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
select(party, votes) %>%
group_by(party) %>%
summarise(votes=sum(votes))
df_pr$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pr$party)
df_pr$party <- gsub("Democraten.66..D66.","D66",df_pr$party)
df_pr$party <- gsub("X50PLUS","Partij 50PLUS",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("SP..Socialistische.Partij.","SP",df_pr$party)
df_pr$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pr$party)
df_pr$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pr$party)
df_pr$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pr$party)
dHondt <- function(votes, parties, n_seats = 150) {
divisor.mat <- sum(votes) / sapply(votes, "/", seq(1, n_seats, 1))
colnames(divisor.mat) <- parties
m.mat <- tidyr::gather(as.data.frame(divisor.mat), key="name", value="value",
everything())
m.mat <- m.mat[rank(m.mat$value, ties.method = "random") <= n_seats, ]
rle.seats <- rle(as.character(m.mat$name))
if (sum(rle.seats$length) != n_seats)
stop(paste("Number of seats distributed not equal to", n_seats))
# fill up the vector with parties that got no seats
if (any(!(parties %in% rle.seats$values))) {
# add parties
missing_parties <- parties[!(parties %in% rle.seats$values)]
for (party in missing_parties) {
rle.seats$lengths <- c(rle.seats$lengths, 0)
rle.seats$values <- c(rle.seats$values, party)
}
# sort results
rle.seats$lengths <- rle.seats$lengths[match(parties, rle.seats$values)]
rle.seats$values <- rle.seats$values[match(parties, rle.seats$values)]
}
rle.seats$length
}
df_pr$seats<- dHondt(df_pr$votes,df_pr$party,150)
df_pr <- df_pr %>%
filter(seats>0) %>%
as.data.frame()
df_pr <- df_pr[order(df_pr$votes,decreasing =T),]
df_pr <- left_join(df_pr,colourscheme)
df_pr$total <- sum(df_pr$votes)
df_pr$percentage <- (df_pr$votes/df_pr$total)*100
df_pr$percentage <- round(df_pr$percentage,1)
resultaat <- df_pr %>%
select(party,votes,seats,percentage)
kable(resultaat)
| party | votes | seats | percentage |
|---|---|---|---|
| VVD | 2279126 | 34 | 22.3 |
| D66 | 1565862 | 24 | 15.3 |
| PVV | 1124482 | 17 | 11.0 |
| CDA | 990601 | 15 | 9.7 |
| SP | 623371 | 9 | 6.1 |
| PvdA | 597192 | 9 | 5.8 |
| GROENLINKS | 537308 | 8 | 5.3 |
| FvD | 523083 | 8 | 5.1 |
| PvdD | 399751 | 6 | 3.9 |
| ChristenUnie | 351275 | 5 | 3.4 |
| Volt | 252480 | 3 | 2.5 |
| JA21 | 246620 | 3 | 2.4 |
| SGP | 215249 | 3 | 2.1 |
| DENK | 211238 | 3 | 2.1 |
| Partij 50PLUS | 106702 | 1 | 1.0 |
| BBB | 104319 | 1 | 1.0 |
| BIJ1 | 87238 | 1 | 0.9 |
Let’s start with examining the composition of the Tweede Kamer - under a proportional representation (D’Hondt) system. This is what the Tweede Kamer looks like currently:
#plot proportional table with ggpol's geom_parliament.
ggplot(df_pr) +
ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") +
#highlight the party in control of the House with a black line
scale_fill_manual(values = df_pr$colour,
labels = df_pr$party)+
coord_fixed()+
theme_void()

Now let’s have a look at what it would look like under First-past-the-Post (FPTP), with one MP elected per Gemeente.
#sort in long format
df_long <- df_results %>%
pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
select(regio =RegioNaam, code = RegioCode,party, votes) %>%
filter(!is.na(votes)) %>%
group_by(code) %>%
mutate(total = sum(votes)) %>%
slice_max(votes)
#convert to percentage, round and remove the G from the codes to match up
df_long$percentage <- (df_long$votes/df_long$total)*100
df_long$percentage <- round(df_long$percentage,1)
df_long$code <- gsub("G","",df_long$code)
#fix the names to acronyms for consistency
df_long$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_long$party)
df_long$party <- gsub("Democraten.66..D66.","D66",df_long$party)
df_long$party <- gsub("X50PLUS","Partij 50PLUS",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("SP..Socialistische.Partij.","SP",df_long$party)
df_long$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_long$party)
df_long$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_long$party)
df_long$party <- gsub("Partij.voor.de.Dieren","PvdD",df_long$party)
df_long$party <- as.factor(df_long$party)
#tidy up region names to exclude -'s and 's
df_long$regio <- gsub("'","",df_long$regio)
df_long$regio <- gsub("-"," ",df_long$regio)
#join names with corresponding colour scheme
df_long <- left_join(df_long,colourscheme)
#Tabulate the results in order to get a seat tally by party
fptp_results <- table(df_long$party) %>% as.data.frame()
colnames(fptp_results) <- c("party","seats")
fptp_results <- left_join(fptp_results,colourscheme)
#use geom_parliament from ggpol to visualise
ggplot(fptp_results) +
ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") +
#highlight the party in control of the House with a black line
scale_fill_manual(values = fptp_results$colour,
labels = fptp_results$party)+
coord_fixed()+
theme_void()

Now, there are a couple of obvious differences:
But also some obvious caveats:
We can also show these results on a map - this in an interactive map which allows you to explore the individual election results for each Gemeente.
#Prepare data for results per gemeente
#sort in long format
df_pie <- df_results %>%
pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
select(regio =RegioNaam, code = RegioCode,party, votes) %>%
filter(!is.na(votes)) %>%
group_by(code) %>%
mutate(total = sum(votes))
df_pie$code <- gsub("G","",df_pie$code)
#bit of code to automatically remove the gemeentes that can be found in the results, but not on the map.
#these are the gemeentes Bonaire, Saba and Sint Eustasius which are in the Caribbean.
names_df <- unique(df_pie$code)
names_shp <- unique(shp_gemeentes$Gemeenteco)
names_remove <- names_df[!names_df %in% names_shp]
#remove Bonaire, Saba, Sint Eustasius
df_pie <- df_pie %>%
filter(!code %in% names_remove)
#round percentages
df_pie$percentage <- (df_pie$votes/df_pie$total)*100
df_pie$percentage <- round(df_pie$percentage,1)
#change name acronyms
#this step is being repeated, initially seperate script - move up to first chunk?
df_pie$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pie$party)
df_pie$party <- gsub("Democraten.66..D66.","D66",df_pie$party)
df_pie$party <- gsub("X50PLUS","Partij 50PLUS",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("SP..Socialistische.Partij.","SP",df_pie$party)
df_pie$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pie$party)
df_pie$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pie$party)
df_pie$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pie$party)
df_pie$party <- as.factor(df_pie$party)
df_pie$party <- as.character(df_pie$party)
df_pie$percentage <- as.numeric(df_pie$percentage)
#make a list of pie chart plots, in the order of code - this way it will correspond with the rows in the shapefile.
pie_plot_list <- lapply(unique(df_pie$code), function(i) {
#filter and group info per gemeente code
regio_df <- df_pie %>%
filter(code == i) %>%
dplyr::ungroup() %>%
select(party,percentage,regio) %>%
filter(percentage >0.1) %>%
as.data.frame()
#order by the vote tally
regio_df <- regio_df[order(regio_df$percentage,decreasing = T),]
#rejoin with the colourscheme
regio_df <- left_join(regio_df,colourscheme)
#plot as a bar chart, important to paste the corresponding gemeente name
ggplot(regio_df) +
geom_bar(aes(x = "", y = percentage, fill = party),
stat = "identity", colour = "black", width =1) +
#geom_text(aes(x = "", y = pct, label = percent(pct)), position = position_stack(vjust = 0.5))+
coord_polar("y", start = 0) +
labs(x = NULL, y = NULL, fill = NULL,
title = paste("Verkiezingsuitslag",unique(regio_df$regio))) +
theme_classic()+
scale_fill_manual(values = regio_df$colour,
limits = regio_df$party)+
theme(axis.line = element_blank(),
axis.text = element_blank(),
axis.ticks = element_blank(),
plot.title = element_text(hjust = 0.5, color = "black"),
legend.position = "bottom")
})
#remove saba, st eustasius etc.
df_long <- df_long %>%
filter(!code %in% names_remove)
#link to spatial
shp_fptp <- left_join(shp_gemeentes, df_long, by = c("Gemeenteco"="code"))
shp_fptp <- shp_fptp %>%
filter(percentage >0.1)
#I need to make the shapefile alphabetical to match with the pie chart list
#This means removing apostrophe's at 's-Gravenhage and 's-Hertogenbosch
shp_fptp$Gemeentena <- gsub("'","",shp_gemeentes$Gemeentena)
#reorder
shp_fptp <- shp_fptp[order(shp_fptp$Gemeentena),]
#reset rownumbers so they line up
rownames(shp_fptp) <- 1:nrow(shp_fptp)
#use mapview to display colour by party, add popupgrah which refers to the list of pie charts made earlier.
mapview(shp_fptp,
zcol = "party",
col.regions = shp_fptp$colour,
popup = popupGraph(pie_plot_list, width = 450,height =300,),
legend = TRUE,
layer.name = "Party")
Clicking on each gemeente will display a pie chart with its individual results. Some geographical patterns to note is that the Christian parties (SGP, CU) tend to do well in the so-called “bible-belt”. More Liberal and Green parties (D66, GroenLinks) perform well in the big cities. In rural areas where agriculture is an important industry, the CDA is a popular choice. The populist right-wing party, the PVV, does well on the fringes of the Netherlands - far from the centre of power where trust in politics is low.